package com.gframework.mybatis.dao.datasource;

import java.io.Closeable;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.transaction.support.TransactionSynchronizationManager;

/**
 * 轻量级多数据源操作实现类.
 * <pre>
 * 执行{@link #setDataSource(String)} 可以进行数据源的切换。
 * 本类有两种操作情况：
 * 1、非事务情况下：
 * 	这种情况本次执行后仅下一次数据库操作有效。因为非事务情况下数据库连接即用即关。
 * 2、事务情况下：
 * 	事务情况下，一个事务只会有一个数据库连接对象，而且会在进入这个事务方法前就获取到，
 * 因此你需要在进入一个新的事务之前执行本类的切换数据源方法才能生效。
 * </pre>
 * <strong>请务必保证在执行完此方法后立刻执行相关操作，这样才会发生不可预料的错误。</strong>
 * <br>
 * <strong>如果对当前执行代码的事务逻辑不是很清楚，请慎用。如果已经处于事务中了，如果调用此类方法，会有警告提示，因为这可能是无效。如果你认为你的代码没有逻辑错误，那么可以忽略此警告！</strong>
 * 
 * 
 * @since 1.0.0
 * @author Ghwolf
 *
 */
public class RoutingDataSource extends AbstractDataSource implements Closeable,DisposableBean {
	private static final Logger slfLog = LoggerFactory.getLogger(RoutingDataSource.class);
	
	/**
	 * 动态数据源管理器
	 */
	@Autowired
	private DynamicDataSourceManager dataSourceManager ;
	
	/**
	 * 默认数据源
	 */
	private final DataSource defaultDataSource ;

	/**
	 * 保存当前线程设置的数据源key，获取一次后删除
	 */
	private static ThreadLocal<Key> dataSourceKey = new ThreadLocal<>();
	
	static class Key {
		String dsKey ;
		boolean always = false ;
		public Key(String dsKey){
			this.dsKey = dsKey;
		}
	}
	
	/**
	 * 实例化本类对象时，需要设置主数据源
	 */
	public RoutingDataSource(DataSource defaultDataSource){
		if (defaultDataSource == null) {
			throw new IllegalArgumentException("主数据源参数不能为null！");
		}
		this.defaultDataSource = defaultDataSource;
	}
	
	/**
	 * 修改下一次获取的数据源对应的key，如果不存在，则会在获取时抛出异常。
	 * @param key 数据源key
	 * @throws DataSourceException 如果已经设置了数据源，则抛出异常。通常来说这说明代码中出现了问题，需要检查。
	 */
	public static void setDataSource(String key) {
		// 当前在事务中且处于事务synchronized状态（既事务没有挂起）
		if (TransactionSynchronizationManager.isActualTransactionActive()) {
			slfLog.warn("当前正处于事务状态，执行数据用修改操作可能是无效的，请检查代码逻辑，如果您的逻辑是正确的，可以无视此警告！");
		}
		if (dataSourceKey.get() != null) {
			slfLog.error("重复设置了数据源，这可能会导致潜在的代码逻辑错误，请检查！");
			throw new DataSourceException("重复设置了数据源，这可能会导致潜在的代码逻辑错误，请检查！");
		}
		dataSourceKey.set(new Key(key));
		if (slfLog.isInfoEnabled()) {
			slfLog.info("设置了一个新的数据源：[{}]，下一个事务或者非事务情况下下一次数据库连接对象将会改变！",key);
		}
	}
	
	
	/**
	 * 持续修改当前线程后续获取的数据源对应的key，直到调用{@link #stopAlwaysSetDataSource()}为止。
	 * <p>如果不存在，则会在获取时抛出异常。
	 * <p>请确保你在调用此方法后，在finally语句块中执行{@link #stopAlwaysSetDataSource()}以关闭持续的dataSource修改，避免造成安全和内存溢出问题。
	 * @param key 数据源key
	 * @throws DataSourceException 如果已经设置了数据源，则抛出异常。通常来说这说明代码中出现了问题，需要检查。
	 */
	public static void alwaysSetDataSource(String key) {
		setDataSource(key);
		dataSourceKey.get().always = true ;
	}
	
	
	/**
	 * 停止{@link #alwaysSetDataSource(String)}操作，并回复在后续操作中依然使用主数据源。
	 */
	public static void stopAlwaysSetDataSource() {
		dataSourceKey.remove();
	}
	
	@Override
	public Connection getConnection() throws SQLException {
		Key key = null;
		try {
			key = dataSourceKey.get();
			if (key == null) {
				return defaultDataSource.getConnection();
			} else {
				return this.dataSourceManager.openConnection(key.dsKey);
			}
		} finally {
			if (key != null && !key.always) {
				dataSourceKey.remove();
			}
		}
	}

	@Override
	public Connection getConnection(String username, String password) throws SQLException {
		Key key = null;
		try {
			key = dataSourceKey.get();
			if (key == null) {
				return defaultDataSource.getConnection(username,password);
			} else {
				return this.dataSourceManager.openConnection(key.dsKey);
			}
		} finally {
			if (key != null && !key.always) {
				dataSourceKey.remove();
			}
		}
	}

	/**
	 * 在任何一个事务中执行此方法后，当前事务无论如何都会执行回滚操作.<br>
	 * 执行此方法的好处就是不需要抛出异常减少性能损耗
	 */
	public static void setRollbackOnly() {
		TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
	}

	@Override
	public void close() throws IOException {
		if (defaultDataSource instanceof Closeable) {
			((Closeable) defaultDataSource).close();
		}
	}

	@Override
	public void destroy() throws Exception {
		this.clone();
	}

}
